package com.cmc.cloud.cmclink.doc.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.cmc.cloud.cmclink.doc.api.document.emums.*;
import com.cmc.cloud.cmclink.doc.constants.CommonConstants;
import com.cmc.cloud.cmclink.doc.constants.errorconstant.SpecialCargoApplyErrorCodeConstants;
import com.cmc.cloud.cmclink.doc.convert.SpecialCargoCasConvert;
import com.cmc.cloud.cmclink.doc.convert.SpecialCargoVoyageConvert;
import com.cmc.cloud.cmclink.doc.entity.*;
import com.cmc.cloud.cmclink.doc.mapper.*;
import com.cmc.cloud.cmclink.doc.service.SpecialCargoApprovalService;
import com.cmc.cloud.cmclink.doc.service.so.SoCommonService;
import com.cmc.cloud.cmclink.doc.service.special.EmailTaskService;
import com.cmc.cloud.cmclink.doc.util.CostChargeUtil;
import com.cmc.cloud.cmclink.doc.util.RedisUtils;
import com.cmc.cloud.cmclink.doc.util.VslvoyUtil;
import com.cmc.cloud.cmclink.doc.vo.sovo.attachmentvo.AttachmentRespVO;
import com.cmc.cloud.cmclink.doc.vo.specialcargoapplyvo.*;
import com.cmc.cloud.cmclink.doc.vo.specialcargocasvo.SpecialCargoCasRespVO;
import com.cmes.framework.common.exception.util.ServiceExceptionUtil;
import com.cmes.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.cmes.framework.security.core.util.SecurityFrameworkUtils;
import com.cmes.platform.system.nuc.NucApi;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;


/**
 * 特种货审批主信息 Service 实现类
 *
 * @author 集运管理员
 */
@Service
@Validated
public class SpecialCargoApprovalServiceImpl implements SpecialCargoApprovalService {

    @Resource
    private SpecialCargoApplyMapper specialCargoApplyMapper;
    @Resource
    private SpecialCargoVoyageMapper specialCargoVoyageMapper;
    @Resource
    private SpecialCargoCntrMapper specialCargoCntrMapper;
    @Resource
    private SpecialCargoOogMapper specialCargoOogMapper;
    @Resource
    private SpecialCargoOdoMapper specialCargoOdoMapper;
    @Resource
    private SpecialCargoDgMapper specialCargoDgMapper;
    @Resource
    private SpecialCargoFlexiMapper specialCargoFlexiMapper;
    @Resource
    private SpecialCargoDgPackingMapper specialCargoDgPackingMapper;
    @Resource
    private SpecialCargoCasMapper specialCargoCasMapper;
    @Resource
    private CargoCntrTypeRelationMapper cargoCntrTypeRelationMapper;
    @Resource
    private AttachmentMapper attachmentMapper;
    @Resource
    private NucApi nucApi;
    @Resource
    private OperateLogMapper operateLogMapper;
    @Resource
    private RedisUtils redisUtils;
    @Resource
    SoCommonService soCommonService;
    @Resource
    private SpecialCargoDgExtMapper specialCargoDgExtMapper;
    @Resource
    private EmailTaskService emailTaskService;
    @Resource
    private VslvoyUtil vslvoyUtil;
    @Resource
    private CostChargeUtil costChargeUtil;
    @Resource
    private ApprovePermissionConfigMapper approvePermissionConfigMapper;

    private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    private static final String CNTR_SIZE_20 = "2";
    private static final String CNTR_SIZE_40 = "4";

    @Override
    public ApprovalDetailRespVO getApprovalDetailInfo(Long id) {
        SpecialCargoApplyDO applyDo = specialCargoApplyMapper.selectById(id);
        if (null == applyDo) {
            throw ServiceExceptionUtil.exception(SpecialCargoApplyErrorCodeConstants.SPECIAL_CARGO_APPLY_NOT_EXISTS);
        }
        LambdaQueryWrapperX<SpecialCargoVoyageDO> yoyageWrapperX = new LambdaQueryWrapperX<>();
        yoyageWrapperX.eq(SpecialCargoVoyageDO::getApplyId, id);
        yoyageWrapperX.orderByAsc(SpecialCargoVoyageDO::getRouteNo);
        List<SpecialCargoVoyageDO> voyages = specialCargoVoyageMapper.selectList(yoyageWrapperX);
        if (CollectionUtil.isEmpty(voyages)) {
            throw ServiceExceptionUtil.exception(SpecialCargoApplyErrorCodeConstants.SPECIAL_APPROVAL_VOYAGE_ERROR);
        }
        List<SpecialCargoCntrDO> cntrDos = specialCargoCntrMapper.selectList(new LambdaQueryWrapper<SpecialCargoCntrDO>().eq(SpecialCargoCntrDO::getApplyId, id)
                .orderByAsc(SpecialCargoCntrDO::getCreateTime));
        if (CollectionUtil.isEmpty(cntrDos)) {
            throw ServiceExceptionUtil.exception(SpecialCargoApplyErrorCodeConstants.SPECIAL_CARGO_CNTR_NOT_EXISTS);
        }
        String applyType = applyDo.getApplyType();
        String approvalRemark = voyages.stream().map(voyage -> (StringUtils.isEmpty(voyage.getApproveRemark()) ? "" : voyage.getApproveRemark())).collect(Collectors.joining("\n"));
        List<SpecialCargoVoyageUpdateReqVO> voyageInfoVo = SpecialCargoVoyageConvert.INSTANCE.convertList(voyages);
        Set<String> vessels = voyages.stream().map(SpecialCargoVoyageDO::getVesselCode).collect(Collectors.toSet());
        vessels.add("NonOwnedShip");
        LambdaQueryWrapper<ApprovePermissionConfigDO> queryWrapper =
                Wrappers.<ApprovePermissionConfigDO>lambdaQuery().eq(ApprovePermissionConfigDO::getUserId, SecurityFrameworkUtils.getLoginUserId())
                        .in(ApprovePermissionConfigDO::getVesselCode, vessels);
        List<ApprovePermissionConfigDO> permissionConfigs = approvePermissionConfigMapper.selectList(queryWrapper);
        Set<String> userVessels = permissionConfigs.stream().map(ApprovePermissionConfigDO::getVesselCode).collect(Collectors.toSet());
        voyageInfoVo.forEach(vo -> {
            vo.setApprovalAuth(false);
            boolean exists = userVessels.contains(vo.getVesselCode());
            if (exists) {
                vo.setApprovalAuth(true);
            }
            if (!CntrOwnerEnum.SNL.getValue().equals(vo.getCarrier()) && userVessels.contains("NonOwnedShip")) {
                vo.setApprovalAuth(true);
            }
        });
        costChargeUtil.setVslvoyTerminal(voyageInfoVo);
        vslvoyUtil.setEtd(voyageInfoVo);
        ApprovalDetailRespVO approval = new ApprovalDetailRespVO();
        approval.setVoyageInfos(voyageInfoVo);
        approval.setCntrDataDisplays(getDetailCntrData(voyages));
        Set<String> carrier = voyages.stream().map(SpecialCargoVoyageDO::getCarrier).collect(Collectors.toSet());
        if (carrier.size() == 1) {
            approval.setVesselOperator(carrier.stream().findFirst().get());
        }
        List<AttachmentRespVO> attachmentList = attachmentMapper.getAttachmentInfos(id, SoSiEnum.SPC.getValue());
        attachmentList.forEach(a -> {
            if (ApprovalStatusEnum.PRECONSENT.getValue().equals(a.getAttachTypeCode())) {
                a.setAttachTypeNameCn("装箱后的绑扎照片");
                a.setAttachTypeNameEn("Photo of Post-Stowage Strapping");
            }
        });
        String attachments = StringUtils.EMPTY;
        if (CollectionUtil.isNotEmpty(attachmentList)) {
            approval.setAttachmentInfos(attachmentList);
            attachments = attachmentList.stream().map(AttachmentRespVO::getAttachName).collect(Collectors.joining(","));
        }
        if (SpecialEnum.OOG.getValue().equals(applyType) || SpecialEnum.AK.getValue().equals(applyType)) {
            approval.setSpecialDetail(generateOogDetailInfo(applyDo, cntrDos, voyages, attachments));
        }
        if (SpecialEnum.ODO.getValue().equals(applyType)) {
            approval.setSpecialDetail(generateOdoDetailInfo(applyDo, cntrDos, voyages, attachments));
        }
        if (SpecialEnum.FLEXI.getValue().equals(applyType)) {
            approval.setSpecialDetail(generateFlexiDetailInfo(applyDo, cntrDos, voyages, attachments));
        }
        if (SpecialEnum.DG.getValue().equals(applyType)) {
            List<SpecialCargoDgDO> dgDos = specialCargoDgMapper.selectList(new LambdaQueryWrapper<SpecialCargoDgDO>().eq(SpecialCargoDgDO::getApplyId, applyDo.getId()));
            if (CollectionUtil.isEmpty(dgDos)) {
                throw ServiceExceptionUtil.exception(SpecialCargoApplyErrorCodeConstants.SPECIAL_NO_DG_ERROR);
            }
            approval.setSpecialDetail(generateDgDetailInfo(applyDo, dgDos, voyages, attachments));
            List<Long> dgIds = dgDos.stream().map(SpecialCargoDgDO::getId).collect(Collectors.toList());
            List<SpecialCargoCasDO> casDos = specialCargoCasMapper.selectList(new LambdaQueryWrapper<SpecialCargoCasDO>().in(SpecialCargoCasDO::getSpecialCargoDgId, dgIds));
            List<SpecialCargoCasRespVO> casInfo = SpecialCargoCasConvert.INSTANCE.convertList(casDos);
            approval.setCasInfos(casInfo);
        }
        approval.setApplyStatus(applyDo.getApplyStatus());
        approval.setApplyRemark(applyDo.getApplyRemark());
        approval.setApprovalRemark(approvalRemark);
        return approval;
    }

    public List<DetailCntrDataDisplay> getDetailCntrData(List<SpecialCargoVoyageDO> voyages) {
        DetailCntrDataReqVO detailCntrDataReqVO = new DetailCntrDataReqVO();
        detailCntrDataReqVO.setVesselCodes(voyages.stream().map(SpecialCargoVoyageDO::getVesselCode).toArray(String[]::new));
        detailCntrDataReqVO.setVoyageExs(voyages.stream().map(SpecialCargoVoyageDO::getVoyageEx).toArray(String[]::new));
        detailCntrDataReqVO.setPolCodes(voyages.stream().map(SpecialCargoVoyageDO::getPolCode).toArray(String[]::new));
        detailCntrDataReqVO.setPodCodes(voyages.stream().map(SpecialCargoVoyageDO::getPodCode).toArray(String[]::new));
        List<DetailCntrDataRespVO> detailCntrData = specialCargoApplyMapper.getDetailCntrData(detailCntrDataReqVO);
        List<DetailCntrDataDisplay> detailList = new ArrayList<>();
        for (SpecialCargoVoyageDO voyage : voyages) {
            DetailCntrDataDisplay detail = new DetailCntrDataDisplay();
            String polCode = voyage.getPolCode();
            String podCode = voyage.getPodCode();
            String vesselCode = voyage.getVesselCode();
            String voyageEx = voyage.getVoyageEx();
            if (!TransmodeEnum.LINER.getValue().equals(voyage.getTransmode())) {
                detail.setVelVoy(vesselCode + "/" + voyageEx);
                detail.setPolPodCode(polCode + "-" + podCode);
                detail.setApprovalTeuNum("-");
                detail.setPendingTeuNum("-");
                detail.setApprovalUnitNum("-");
                detail.setPendingUnitNum("-");
                detailList.add(detail);
                continue;
            }
            //20尺寸审批的箱子数
            int approve20Num = detailCntrData.stream()
                    .filter(vo -> polCode.equals(vo.getPolCode())
                            && podCode.equals(vo.getPodCode())
                            && ApprovalStatusEnum.APPROVED.getValue().equalsIgnoreCase(vo.getApproveStatus())
                            && vo.getCntrSize().startsWith(CNTR_SIZE_20))
                    .mapToInt(DetailCntrDataRespVO::getCntrNum).sum();
            //40尺寸审批的箱子数
            int approve40Num = detailCntrData.stream()
                    .filter(vo -> polCode.equals(vo.getPolCode())
                            && podCode.equals(vo.getPodCode())
                            && ApprovalStatusEnum.APPROVED.getValue().equalsIgnoreCase(vo.getApproveStatus())
                            && vo.getCntrSize().startsWith(CNTR_SIZE_40))
                    .mapToInt(DetailCntrDataRespVO::getCntrNum).sum();
            //20待审批箱子数
            int pending20Num = detailCntrData.stream()
                    .filter(vo -> polCode.equals(vo.getPolCode())
                            && podCode.equals(vo.getPodCode())
                            && ApprovalStatusEnum.PENDING.getValue().equalsIgnoreCase(vo.getApproveStatus())
                            && vo.getCntrSize().startsWith(CNTR_SIZE_20))
                    .mapToInt(DetailCntrDataRespVO::getCntrNum).sum();
            //40待审批箱子数
            int pending40Num = detailCntrData.stream()
                    .filter(vo -> polCode.equals(vo.getPolCode())
                            && podCode.equals(vo.getPodCode())
                            && ApprovalStatusEnum.PENDING.getValue().equalsIgnoreCase(vo.getApproveStatus())
                            && vo.getCntrSize().startsWith(CNTR_SIZE_40))
                    .mapToInt(DetailCntrDataRespVO::getCntrNum).sum();
            detail.setVelVoy(vesselCode + "/" + voyageEx);
            detail.setPolPodCode(polCode + "-" + podCode);
            detail.setApprovalTeuNum(String.valueOf(approve20Num + 2 * approve40Num));
            detail.setPendingTeuNum(String.valueOf(pending20Num + 2 * pending40Num));
            detail.setApprovalUnitNum(String.valueOf(approve20Num + approve40Num));
            detail.setPendingUnitNum(String.valueOf(pending20Num + pending40Num));
            detailList.add(detail);
        }
        return detailList;
    }


    public String generateOogDetailInfo(SpecialCargoApplyDO applyDO, List<SpecialCargoCntrDO> cntrDos, List<SpecialCargoVoyageDO> voyageDos, String attachments) {
        List<Long> cntrIds = cntrDos.stream().map(SpecialCargoCntrDO::getId).collect(Collectors.toList());
        List<SpecialCargoOogDO> oogDos = specialCargoOogMapper.selectList(new LambdaQueryWrapper<SpecialCargoOogDO>().in(SpecialCargoOogDO::getCntrId, cntrIds));
        if (CollectionUtil.isEmpty(oogDos)) {
            throw ServiceExceptionUtil.exception(SpecialCargoApplyErrorCodeConstants.SPECIAL_NO_OOG_ERROR);
        }
        Map<Long, SpecialCargoOogDO> oogDoMap = oogDos.stream().collect(Collectors.toMap(SpecialCargoOogDO::getCntrId, e -> e));

        StringBuilder notice = commonHeadInfo();
        notice.append("<p>AWKWARD CARGO    ").append(SDF.format(applyDO.getApplyTime())).append("</p>\n");
        notice.append("*****************\n\n");
        // 基本信息部分
        notice.append("<p>BOOKING NUMBER: ").append(applyDO.getBookingNo()).append("</p>\n");
        for (SpecialCargoVoyageDO voyage : voyageDos) {
            String vesVoy = voyage.getVesselCode() + " " + voyage.getVoyageEx();
            notice.append("<p>VSL NAME/VOYAGE: ").append(vesVoy).append("</p>\n");
            notice.append("<p>POL: ").append(voyage.getPolCode()).append("</p>\n");
            notice.append("<p>POD: ").append(voyage.getPodCode()).append("</p>\n");
        }
        notice.append("<p>DEL: ").append(applyDO.getDelCode()).append("</p>\n");
        notice.append("<p>CARGO DETAILS:</p>\n");
        for (SpecialCargoCntrDO cntr : cntrDos) {
            SpecialCargoOogDO oogDO = oogDoMap.get(cntr.getId());
            if (null == oogDO) {
                continue;
            }
            notice.append("<p>CONTAINER QTY / TPSZ:").append(cntr.getQty()).append("×").append(cntr.getCntrSize()).append(cntr.getCntrType()).append("</p>\n\n");
            notice.append("--------------------------------------\n\n");
            if (CommonConstants.CONTAINER_20HC.equals(cntr.getCntrSize() + cntr.getCntrType()) || CommonConstants.CONTAINER_45HC.equals(cntr.getCntrSize() + cntr.getCntrType())) {
                notice.append("<p>&nbsp;&nbsp; CORNER POST STATUS : ").append(oogDO.getCornerPostStatus()).append("</p>\n");
                notice.append("<p>&nbsp;&nbsp; SPECIAL STOWAGE REQUIREMENT : ").append(oogDO.getSpecialStorageRequire()).append("</p>\n\n");
                continue;
            }
            notice.append("<p>&nbsp;&nbsp; COMMODITY: ").append(oogDO.getCargoDescription()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; GROSS WEIGHT :").append(cntr.getGrossWgt()).append("KGS</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; NET WEIGHT :").append(cntr.getNetWgt()).append("KGS</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; OVERALL DIMENSION(IN CM) : (LENGTH × WIDTH × HEIGHT)</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; 1) TOTAL : ").append(oogDO.getOverallLength()).append("×").append(oogDO.getOverallWidth()).append("×").append(oogDO.getOverallHeight()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; OVER DIMENSION(IN CM) :</p>\n");
            notice.append("<p>&nbsp;&nbsp; 1) FORWARD : ").append(oogDO.getOverForward()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; 2) AFTER  : ").append(oogDO.getOverAfter()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; 3) HEIGHT : ").append(oogDO.getOverHeight()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; 4) LEFT (PORT SIDE) : ").append(oogDO.getOverLeft()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; 5) RIGHT (STARBOARD) : ").append(oogDO.getOverRight()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; CORNER POST STATUS : ").append(oogDO.getCornerPostStatus()).append("</p>\n");
            notice.append("<p>&nbsp;&nbsp; SPECIAL STOWAGE REQUIREMENT : ").append(oogDO.getSpecialStorageRequire()).append("</p>\n\n");
        }
        notice.append(commonAttachmentInfo(attachments));
        notice.append(commonEndInfo());
        return notice.toString();
    }

    public String generateOdoDetailInfo(SpecialCargoApplyDO applyDO, List<SpecialCargoCntrDO> cntrDos, List<SpecialCargoVoyageDO> voyageDos, String attachments) {
        List<Long> cntrIds = cntrDos.stream().map(SpecialCargoCntrDO::getId).collect(Collectors.toList());
        List<SpecialCargoOdoDO> odoDos = specialCargoOdoMapper.selectList(new LambdaQueryWrapper<SpecialCargoOdoDO>().in(SpecialCargoOdoDO::getCntrId, cntrIds));
        if (CollectionUtil.isEmpty(odoDos)) {
            throw ServiceExceptionUtil.exception(SpecialCargoApplyErrorCodeConstants.SPECIAL_NO_ODO_ERROR);
        }
        Map<Long, SpecialCargoOdoDO> odoDoMap = odoDos.stream().collect(Collectors.toMap(SpecialCargoOdoDO::getCntrId, e -> e));
        StringBuilder notice = commonHeadInfo();
        notice.append("<p>ODO CARGO    ").append(SDF.format(applyDO.getApplyTime())).append("</p>\n");
        notice.append("*****************\n\n");
        notice.append("<p>BOOKING NUMBER: ").append(applyDO.getBookingNo()).append("</p>\n");
        for (SpecialCargoVoyageDO voyage : voyageDos) {
            String vesVoy = voyage.getVesselCode() + " " + voyage.getVoyageEx();
            notice.append("<p>VSL NAME/VOYAGE: ").append(vesVoy).append("</p>\n");
            notice.append("<p>POL: ").append(voyage.getPolCode()).append("</p>\n");
            notice.append("<p>POD: ").append(voyage.getPodCode()).append("</p>\n");
        }
        notice.append("<p>DEL: ").append(applyDO.getDelCode()).append("</p>\n");
        notice.append("<p>CARGO DETAILS:</p>\n");
        for (SpecialCargoCntrDO cntr : cntrDos) {
            SpecialCargoOdoDO odoDO = odoDoMap.get(cntr.getId());
            if (null == odoDO) {
                continue;
            }
            notice.append("<p>CONTAINER QTY / TPSZ:").append(cntr.getQty()).append("×").append(cntr.getCntrSize()).append(cntr.getCntrType()).append("</p>\n\n");
            notice.append("--------------------------------------\n\n");
            notice.append("<p>&nbsp;&nbsp; COMMODITY: ").append(odoDO.getCargoDescription()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; GROSS WEIGHT :").append(cntr.getGrossWgt()).append("KGS</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; NET WEIGHT :").append(cntr.getNetWgt()).append("KGS</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; DOOR OPEN LEFT OR RIGHT :").append(odoDO.getOpenDirection()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; DOOR :").append(odoDO.getDoorRemove()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; OPEN ANGLE :").append(odoDO.getOpenAngle()).append("</p>\n\n");
        }
        notice.append(commonAttachmentInfo(attachments));
        notice.append(commonEndInfo());
        return notice.toString();
    }

    public String generateFlexiDetailInfo(SpecialCargoApplyDO applyDO, List<SpecialCargoCntrDO> cntrDos, List<SpecialCargoVoyageDO> voyageDos, String attachments) {
        List<Long> cntrIds = cntrDos.stream().map(SpecialCargoCntrDO::getId).collect(Collectors.toList());
        List<SpecialCargoFlexiDO> flexiDos = specialCargoFlexiMapper.selectList(new LambdaQueryWrapper<SpecialCargoFlexiDO>().in(SpecialCargoFlexiDO::getCntrId, cntrIds));
        if (CollectionUtil.isEmpty(flexiDos)) {
            throw ServiceExceptionUtil.exception(SpecialCargoApplyErrorCodeConstants.SPECIAL_NO_FLEXI_ERROR);
        }
        Map<Long, SpecialCargoFlexiDO> flexiDoMap = flexiDos.stream().collect(Collectors.toMap(SpecialCargoFlexiDO::getCntrId, e -> e));
        StringBuilder notice = commonHeadInfo();
        notice.append("<p>FLEXITANK CARGO    ").append(SDF.format(applyDO.getApplyTime())).append("</p>\n");
        notice.append("*****************\n\n");
        notice.append("<p>BOOKING NUMBER: ").append(applyDO.getBookingNo()).append("</p>\n");
        for (SpecialCargoVoyageDO voyage : voyageDos) {
            String vesVoy = voyage.getVesselCode() + " " + voyage.getVoyageEx();
            notice.append("<p>VSL NAME/VOYAGE: ").append(vesVoy).append("</p>\n");
            notice.append("<p>POL: ").append(voyage.getPolCode()).append("</p>\n");
            notice.append("<p>POD: ").append(voyage.getPodCode()).append("</p>\n");
        }
        notice.append("<p>DEL: ").append(applyDO.getDelCode()).append("</p>\n");
        notice.append("<p>CARGO DETAILS:</p>\n");
        for (SpecialCargoCntrDO cntr : cntrDos) {
            SpecialCargoFlexiDO flexiDO = flexiDoMap.get(cntr.getId());
            if (null == flexiDO) {
                continue;
            }
            notice.append("<p>CONTAINER QTY / TPSZ:").append(cntr.getQty()).append("×").append(cntr.getCntrSize()).append(cntr.getCntrType()).append("</p>\n\n");
            notice.append("--------------------------------------\n\n");
            notice.append("<p>&nbsp;&nbsp; COMMODITY: ").append(flexiDO.getCargoDescription()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; GROSS WEIGHT :").append(cntr.getGrossWgt()).append("KGS</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; NET WEIGHT :").append(cntr.getNetWgt()).append("KGS</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; CAPACITY OF FLEXITANK (LITRES) :").append(flexiDO.getCapacityOfFlexitank()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; VOLUME OF CARGO (LITRES) :").append(flexiDO.getVolumeOfCargo()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; LOADING RATIO :").append(flexiDO.getLoadingRatio()).append("%</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; PRODUCT NAME :").append(" ").append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; DENSITY :").append(" ").append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; NAME OF THE FLEXITANK MANUFACTURER :").append(flexiDO.getNameOfManufacturer()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; LOADING REQUIREMENTS / HANDLING :").append(" ").append("</p>\n\n");
        }
        notice.append(commonAttachmentInfo(attachments));
        notice.append(commonEndInfo());
        return notice.toString();
    }

    public String generateDgDetailInfo(SpecialCargoApplyDO applyDo, List<SpecialCargoDgDO> dgDos, List<SpecialCargoVoyageDO> voyageDos, String attachments) {
        Map<Long, SpecialCargoDgDO> dgMap = dgDos.stream().collect(Collectors.toMap(SpecialCargoDgDO::getId, e -> e));
        List<Long> dgIds = dgDos.stream().map(SpecialCargoDgDO::getId).collect(Collectors.toList());
        Map<Long, List<SpecialCargoCasDO>> dgCasMap = new HashMap<>();
        if (applyDo.getTransitCnsha()) {
            List<SpecialCargoCasDO> specialCargoCasDos = specialCargoCasMapper.selectList(new LambdaQueryWrapper<SpecialCargoCasDO>().in(SpecialCargoCasDO::getSpecialCargoDgId, dgIds));
            if (CollectionUtil.isNotEmpty(specialCargoCasDos)) {
                dgCasMap = specialCargoCasDos.stream().collect(Collectors.groupingBy(SpecialCargoCasDO::getSpecialCargoDgId));
            }
        }
        List<CargoCntrTypeRelationDO> relationDos = cargoCntrTypeRelationMapper.selectList(new LambdaQueryWrapper<CargoCntrTypeRelationDO>()
                .eq(CargoCntrTypeRelationDO::getApplyId, applyDo.getId()).in(CargoCntrTypeRelationDO::getDgId, dgIds));
        Map<Long, List<CargoCntrTypeRelationDO>> dgCntrMap = relationDos.stream().collect(Collectors.groupingBy(CargoCntrTypeRelationDO::getDgId));
        StringBuilder notice = commonHeadInfo();
        notice.append("<p>DANGEROUS CARGO    ").append(SDF.format(applyDo.getApplyTime())).append("</p>\n");
        notice.append("*****************\n\n");
        notice.append("<p>BOOKING NUMBER: ").append(applyDo.getBookingNo()).append("</p>\n");
        for (SpecialCargoVoyageDO voyage : voyageDos) {
            String vesVoy = voyage.getVesselCode() + " " + voyage.getVoyageEx();
            notice.append("<p>VSL NAME/VOYAGE: ").append(vesVoy).append("</p>\n");
            notice.append("<p>POL: ").append(voyage.getPolCode()).append("</p>\n");
            notice.append("<p>POD: ").append(voyage.getPodCode()).append("</p>\n");
        }
        notice.append("<p>DEL: ").append(applyDo.getDelCode()).append("</p>\n");
        notice.append("<p>CARGO DETAILS:</p>\n");
        for (Long dgId : dgCntrMap.keySet()) {
            List<CargoCntrTypeRelationDO> dgCntr = dgCntrMap.get(dgId);
            List<Long> cntrIds = dgCntr.stream().map(CargoCntrTypeRelationDO::getCntrId).collect(Collectors.toList());
            List<SpecialCargoCntrDO> cntrDos = specialCargoCntrMapper.selectList(new LambdaQueryWrapper<SpecialCargoCntrDO>()
                    .eq(SpecialCargoCntrDO::getApplyId, applyDo.getId()).in(SpecialCargoCntrDO::getId, cntrIds));
            Map<String, List<SpecialCargoCntrDO>> cntrMap = cntrDos.stream().collect(Collectors.groupingBy(cntr -> cntr.getCntrSize() + "_" + cntr.getCntrType()));
            for (String cntrSizeType : cntrMap.keySet()) {
                List<SpecialCargoCntrDO> specialCargoCntrDos = cntrMap.get(cntrSizeType);
                BigDecimal grossWt = BigDecimal.ZERO;
                BigDecimal netWt = BigDecimal.ZERO;
                Integer cntrNum = 0;
                for (SpecialCargoCntrDO cntrInfo : specialCargoCntrDos) {
                    grossWt = grossWt.add(cntrInfo.getGrossWgt());
                    netWt = netWt.add(cntrInfo.getNetWgt());
                    cntrNum = cntrNum + cntrInfo.getQty();
                }
                notice.append("<p>CONTAINER QTY / TPSZ:").append(cntrNum).append("×").append(cntrSizeType).append("</p>\n\n");
                notice.append("<p>&nbsp;&nbsp; &nbsp;&nbsp; GROSS WT :").append(grossWt).append("KGS</p>\n\n");
                notice.append("<p>&nbsp;&nbsp; &nbsp;&nbsp; NET WT :").append(netWt).append("KGS</p>\n\n");
            }
            SpecialCargoDgDO dg = dgMap.get(dgId);
            List<SpecialCargoCasDO> casDos = dgCasMap.get(dgId);
            List<SpecialCargoDgPackingDO> packingDos = specialCargoDgPackingMapper.selectList(new LambdaQueryWrapperX<SpecialCargoDgPackingDO>().eq(SpecialCargoDgPackingDO::getDgId, dgId));
            String innerPacking = packingDos.stream().filter(packing -> PackingEnum.INNER.getValue().equals(packing.getPackingType())).map(SpecialCargoDgPackingDO::getPackings).collect(Collectors.joining(","));
            String outerPacking = packingDos.stream().filter(packing -> PackingEnum.OUTER.getValue().equals(packing.getPackingType())).map(SpecialCargoDgPackingDO::getPackings).collect(Collectors.joining(","));
            notice.append("--------------------------------------\n\n");
            notice.append("<p>&nbsp;&nbsp; IMO CLASS :").append(dg.getImoClass()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; UN NO. :").append(dg.getUnNo()).append("</p>\n\n");
            if (CollectionUtil.isNotEmpty(casDos)) {
                String casNos = casDos.stream().map(SpecialCargoCasDO::getCasNo).collect(Collectors.joining(","));
                notice.append("<p>Cas No.:").append(casNos).append("</p>\n\n");
            }
            notice.append("<p>&nbsp;&nbsp; PROPER SHIPPING NAME :").append(dg.getPsn()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; TECHNICAL NAME :").append(dg.getTechnicalName()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; PACKING GROUP :").append(dg.getPackingGroup()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; SUBSIDIARY RISK :").append(dg.getSubsidiaryRisk()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; DG GROSS WT. :").append(dg.getDgGrossWgt()).append("KGS</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; DG NET WT. :").append(dg.getDgNetWgt()).append("KGS</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; NET EXPLOSIVE WT. :").append(dg.getNetExplosiveWgt()).append("KGS</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; OUTER PACKAGINGS :").append(outerPacking).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; INNER PACKAGINGS :").append(innerPacking).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; SEGREGATION GROUP :").append(dg.getSegregationGroup()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; LIMITED QUANTITY :").append(dg.getLimitedQty()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; FLASH POINT :").append(dg.getFlashPoint()).append("℃</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; MARINE POLLUTANT :").append(dg.getMarinePollutant()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; EMS TABLE NUMBER :").append(dg.getEmsNo()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; ERG :").append(dg.getErg()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; EMERGENCY CONTACT :")
                    .append(dg.getEmergencyContactName()).append(";")
                    .append(dg.getEmergencyContactAddress()).append(";")
                    .append(dg.getEmergencyContactMobile())
                    .append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; MFAGNO :").append(dg.getMfagNo()).append("</p>\n\n");
            notice.append("<p>&nbsp;&nbsp; CARGOSTATUS :").append(dg.getCargoStatus()).append("</p>\n\n");
            List<SpecialCargoDgExtDO> dgExtDOList = specialCargoDgExtMapper.selectList(new LambdaQueryWrapperX<SpecialCargoDgExtDO>().eq(SpecialCargoDgExtDO::getDgId, dg.getId()));
            if (CollectionUtil.isNotEmpty(dgExtDOList)) {
                for (SpecialCargoDgExtDO dgExtDo : dgExtDOList) {
                    notice.append("<p>&nbsp;&nbsp; ").append(dgExtDo.getExtName()).append("&nbsp:&nbsp").append(dgExtDo.getExtValue()).append("℃</p>\n\n");
                }
            }
        }
        notice.append(commonAttachmentInfo(attachments));
        notice.append(commonEndInfo());
        return notice.toString();
    }

    public StringBuilder commonHeadInfo() {
        StringBuilder notice = new StringBuilder();
        notice.append("<p>FM   :<SPECIAL-CARGO@CMHK.COM> ON BEHALF OF SNL SPECIAL CARGO DESK</p>\n");
        notice.append("<br><br>");
        notice.append("<p>TO: PARTNER</p>\n");
        notice.append("<p>FM: SNL</p>\n");
        notice.append("<br><br>");
        notice.append("<p>DEAR PARTNER</p>\n").append("<br>");
        notice.append("<p>WOULD YOU PLS CHECK AND CONFIRM TO LOAD THIS CARGO ON CAPTIONED VESSEL</p>\n");
        notice.append("<br><br>");
        return notice;
    }

    public StringBuilder commonEndInfo() {
        StringBuilder notice = new StringBuilder();
        notice.append("<br>");
        notice.append("<p>BEST REGARDS,</p>");
        notice.append("<p>SNL SPECIAL CARGO DESK</p>");
        notice.append("<p>E-MAIL: SPECIAL-CARGO@CMHK.COM</p>");
        return notice;
    }

    public StringBuilder commonAttachmentInfo(String attachments) {
        StringBuilder notice = new StringBuilder();
        if (StringUtils.isEmpty(attachments)) {
            return notice.append(StringUtils.EMPTY);
        }
        notice.append("<p>&nbsp;&nbsp;注：客户提供的附件有</p>");
        notice.append("<p>").append(attachments).append("</p>");
        return notice;
    }

    @Override
    public String showReplay(Long id) {
        SpecialCargoApplyDO applyDO = specialCargoApplyMapper.selectById(id);
        String applyStatus = applyDO.getApplyStatus();
        if (!(ApplyStatusEnum.APPROVED.getValue().equals(applyStatus) || ApplyStatusEnum.REJECT.getValue().equals(applyStatus))) {
            return StringUtils.EMPTY;
        }
        List<SpecialCargoVoyageDO> voyageDOList = specialCargoVoyageMapper.selectList(new LambdaQueryWrapper<SpecialCargoVoyageDO>()
                .eq(SpecialCargoVoyageDO::getApplyId, id).orderByAsc(SpecialCargoVoyageDO::getRouteNo));
        if (CollectionUtil.isEmpty(voyageDOList)) {
            throw ServiceExceptionUtil.exception(SpecialCargoApplyErrorCodeConstants.SPECIAL_APPROVAL_VOYAGE_ERROR);
        }
        List<SpecialCargoCntrDO> cntrDOList = specialCargoCntrMapper.selectList(new LambdaQueryWrapper<SpecialCargoCntrDO>().eq(SpecialCargoCntrDO::getApplyId, id));
        List<Long> cntrIds = cntrDOList.stream().map(SpecialCargoCntrDO::getId).collect(Collectors.toList());
        String applyType = applyDO.getApplyType();
        StringBuilder notice = new StringBuilder();
        if (SpecialEnum.OOG.getValue().equals(applyType) || SpecialEnum.AK.getValue().equals(applyType)) {
            if (ApplyStatusEnum.APPROVED.getValue().equals(applyStatus)) {
                notice.append("<p>Dear Partner,</p>\n");
                notice.append("<p>Confirm accept below AK/OOG with Approval NO.").append(applyDO.getApplyNo()).append(" (your Reference No.").append(applyDO.getReferenceNo()).append(")").append("</p>\n");
                notice.append("<p>Shipper :").append(applyDO.getSpaceOwner()).append("</p>\n");
                notice.append("<p>Application type : AK/OOG</p>\n");
                notice.append("<p> Reference No.:").append(applyDO.getReferenceNo()).append("</p>\n");
                for (SpecialCargoVoyageDO voyage : voyageDOList) {
                    String vesVoy = voyage.getVesselCode() + " " + voyage.getVoyageEx();
                    notice.append("<p>&nbsp;&nbsp; Vessel and Voyage number YM ").append(vesVoy).append("</p>\n");
                    notice.append("<p>&nbsp;&nbsp; POL: ").append(voyage.getPolCode()).append("</p>\n");
                    notice.append("<p>&nbsp;&nbsp; POD: ").append(voyage.getPodCode()).append("</p>\n");
                }
                notice.append("<p>However please assist to make sure cargo has been safely block, brace and lashed to avoid cargo glide during sea transportation.</p>\n");
                notice.append("<p>Please note that OOG cargo(es) would be loaded on deck if not available in hold and lack of base cargo.</p>\n");
                notice.append("<p>In case cargo(es) is(are) damaged due to loading on deck space by above reason.</p>\n");
                notice.append("<p>Said cargo carried on deck with shipper's consent and entirely at risk of shipper/receiver.</p>\n");
                notice.append("<p>We have no responsibility whatsoever for any loss or damage due to carriage of goods on deck.</p>\n");
                notice.append("<p>Please be noted that master / chief officer can reject load this cargo(es) in case of incongruent lashing / securing condition.</p>\n");
            }
            if (ApplyStatusEnum.REJECT.getValue().equals(applyStatus)) {
                String rejectReason = voyageDOList.stream().filter(voyage -> ApplyStatusEnum.REJECT.getValue().equals(voyage.getRouteApproveStatus()))
                        .findFirst().get().getApproveRemark();
                notice.append("<p>Dear Partner,</p>\n");
                notice.append("<p>Please refer to the result and notice. If any questions or advice, please feel free to contact with SNL Special Cargo Management Team (special-cargo@cmhk.com). Thank you.</p>\n");
                notice.append("<p>We are sorry to reject your application because ").append(rejectReason).append(".</p>\n");
                notice.append("<p>Shipper :").append(applyDO.getSpaceOwner()).append("</p>\n");
                notice.append("<p>Application type : AK/OOG</p>\n");
                notice.append("<p> Reference No.:").append(applyDO.getReferenceNo()).append("</p>\n");
                for (SpecialCargoVoyageDO voyage : voyageDOList) {
                    String vesVoy = voyage.getVesselCode() + " " + voyage.getVoyageEx();
                    notice.append("<p>&nbsp;&nbsp; Vessel and Voyage number YM ").append(vesVoy).append("</p>\n");
                    notice.append("<p>&nbsp;&nbsp; POL: ").append(voyage.getPolCode()).append("</p>\n");
                    notice.append("<p>&nbsp;&nbsp; POD: ").append(voyage.getPodCode()).append("</p>\n");
                }
            }
        }
        if (SpecialEnum.ODO.getValue().equals(applyType)) {
            notice.append("<p>CONFIRM ACCEPT BELOW O.O.G CGO.APPROVE NO.").append(applyDO.getApplyNo()).append(" (YOUR REFERRENCE NO ").append(applyDO.getReferenceNo()).append(")</p>\n");
            notice.append("<p>HOWEVER PLEASE ASSIT TO MAKE SURE</p>\n");
            notice.append("<p>CARGO HAS BEEN SAFELY BLOCK, BRACE AND LASHED</p>\n");
            notice.append("<p>TO AVOID CARGO GLIDE DURING SEA TRANSPORTATION.</p>\n").append("<br>");
            notice.append("<p>PLS BE NOTED THAT OOG CARGO(ES) WOULD BE LOADED ON DECK IF NOT AVAILABLE IN HOLD AND LACK OF BASE CARGO.</p>\n");
            notice.append("<p>IN CASE CARGO(ES) IS(ARE) DAMAGED DUE TO LOADING ON DECK SPACE BY ABOVE REASON.</p>\n");
            notice.append("<p>SAID CARGO CARRIED ON DECK WITH SHIPPER'S CONSENT AND ENTIRELY AT RISK OF SHIPPER/RECEIVER.</p>\n");
            notice.append("<p>WE HAVE NO RESPONSIBILITY WHATSOEVER FOR ANY LOSS OR DAMAGE DUE TO CARRIAGE OF GOODS ON DECK.</p>\n").append("<br>");
            notice.append("<p>PLS BE NOTED THAT MASTER / CHIEF OFFICER CAN REJECT TO LOAD THIS CARGO(ES) IN CASE OF INCONGRUENT LASHING/SECURING CONDITION.</p>\n");
        }
        if (SpecialEnum.FLEXI.getValue().equals(applyType)) {
            List<SpecialCargoFlexiDO> flexiDOList = specialCargoFlexiMapper.selectList(new LambdaQueryWrapper<SpecialCargoFlexiDO>().in(SpecialCargoFlexiDO::getCntrId, cntrIds));
            Map<Long, SpecialCargoFlexiDO> cntr2FlexiMap = flexiDOList.stream().collect(Collectors.toMap(SpecialCargoFlexiDO::getCntrId, e -> e));
            if (ApplyStatusEnum.APPROVED.getValue().equals(applyStatus)) {
                notice.append("<p>Dear Partner,</p>\n");
                notice.append("<p>Please refer to the result and notice. If any questions or advice, please feel free to contact with SNL Special Cargo Management Team (special-cargo@cmhk.com). Thank you.</p>\n");
                notice.append("<p>Shipper :").append(applyDO.getSpaceOwner()).append("</p>\n");
                notice.append("<p>Application type : Flexitank</p>\n");
                notice.append("<p> Reference No.:").append(applyDO.getReferenceNo()).append("</p>\n");
                for (SpecialCargoVoyageDO voyage : voyageDOList) {
                    String vesVoy = voyage.getVesselCode() + " " + voyage.getVoyageEx();
                    notice.append("<p>&nbsp;&nbsp; Vessel and Voyage number YM ").append(vesVoy).append("</p>\n");
                    notice.append("<p>&nbsp;&nbsp; POL: ").append(voyage.getPolCode()).append("</p>\n");
                    notice.append("<p>&nbsp;&nbsp; POD: ").append(voyage.getPodCode()).append("</p>\n");
                }
                notice.append("<p>Accepted (Approval No. <b>").append(applyDO.getApplyNo()).append("</b>), subject to following requests:</p>\n");
                notice.append("<p>It is a non-hazardous cargo / non-marine pollutant and on deck during vessel space is available.</p>\n");
                notice.append("<p>&nbsp;&nbsp; 1.Remark stowage request “flexitank - on deck in the midst of vessel” and provide container No. in the final CBF/TBU.</p>\n");
                notice.append("<p>&nbsp;&nbsp; 2.Provide container No. for further arrangement once available, or it might be rejected in the final.</p>\n");
                notice.append("<p>&nbsp;&nbsp; 3.Said cargo might need to arrange temporary stowage in case short of sufficient space at the loading port, and some restows might be involved in the following ports; all the extra costs will be for the shipper’s account.</p>\n");
                notice.append("<p>&nbsp;&nbsp; 4.The shipper shall be responsible for leakage, damage, restow, fine, creating spaces issue, and the incident caused by said cargo. All extra costs will be for the shipper's account.</p>\n");
                notice.append("<br><br>");
                notice.append("<p>--GOODS ITEM--</p>\n");
                notice.append("<br><br>");
                notice.append("<table border=\"1\" cellpadding=\"5\" cellspacing=\"0\">\n");
                notice.append("<tr>\n");
                notice.append("<th><b>CNTR. Size</b></th>\n");
                notice.append("<th><b>CNTR. Type</b></th>\n");
                notice.append("<th><b>QTY.</b></th>\n");
                notice.append("<th><b>Gross Weight(kg)</b></th>\n");
                notice.append("<th><b>Manufacturer of Flexitank</b></th>\n");
                notice.append("<th><b>Stowage Request</b></th>\n");
                notice.append("<th><b>Commodity</b></th>\n");
                notice.append("</tr>\n");
                for (SpecialCargoCntrDO cntrDO : cntrDOList) {
                    notice.append("<tr>\n");
                    notice.append("<td>").append(cntrDO.getCntrSize()).append("</td>\n");
                    notice.append("<td>").append(cntrDO.getCntrType()).append("</td>\n");
                    notice.append("<td>").append(cntrDO.getQty()).append("</td>\n");
                    notice.append("<td>").append(cntrDO.getGrossWgt()).append("</td>\n");
                    SpecialCargoFlexiDO flexiDO = cntr2FlexiMap.get(cntrDO.getId());
                    notice.append("<td>").append(flexiDO.getNameOfManufacturer()).append("</td>\n");
                    notice.append("<td>").append("Stowage Request").append("</td>\n");
                    notice.append("<td>").append(flexiDO.getCargoDescription()).append("</td>\n");
                    notice.append("</tr>\n");
                }
                notice.append("</table>");
            }
            if (ApplyStatusEnum.REJECT.getValue().equals(applyStatus)) {
                String rejectReason = voyageDOList.stream().filter(voyage -> ApplyStatusEnum.REJECT.getValue().equals(voyage.getRouteApproveStatus()))
                        .findFirst().get().getApproveRemark();
                notice.append("<p>Dear Partner,</p>\n");
                notice.append("<p>Please refer to the result and notice. If any questions or advice, please feel free to contact with SNL Special Cargo Management Team (special-cargo@cmhk.com). Thank you.</p>\n");
                notice.append("<p>We are sorry to reject your application because ").append(rejectReason).append(".</p>\n");
                notice.append("<p>Shipper :").append(applyDO.getSpaceOwner()).append("</p>\n");
                notice.append("<p>Application type : Flexitank</p>\n");
                notice.append("<p> Reference No.:").append(applyDO.getReferenceNo()).append("</p>\n");
                for (SpecialCargoVoyageDO voyage : voyageDOList) {
                    String vesVoy = voyage.getVesselCode() + " " + voyage.getVoyageEx();
                    notice.append("<p>&nbsp;&nbsp; Vessel and Voyage number YM ").append(vesVoy).append("</p>\n");
                    notice.append("<p>&nbsp;&nbsp; POL: ").append(voyage.getPolCode()).append("</p>\n");
                    notice.append("<p>&nbsp;&nbsp; POD: ").append(voyage.getPodCode()).append("</p>\n");
                }
                notice.append("<br><br>");
                notice.append("<p>--GOODS ITEM--</p>\n");
                notice.append("<br><br>");
                notice.append("<table border=\"1\" cellpadding=\"5\" cellspacing=\"0\">\n");
                notice.append("<tr>\n");
                notice.append("<th><b>CNTR.Size</b></th>\n");
                notice.append("<th><b>CNTR.Type</b></th>\n");
                notice.append("<th><b>QTY.</b></th>\n");
                notice.append("<th><b>Gross Weight(kg)</b></th>\n");
                notice.append("<th><b>Manufacturer of Flexitank</b></th>\n");
                notice.append("<th><b>Stowage Request</b></th>\n");
                notice.append("<th><b>Commodity</b></th>\n");
                notice.append("</tr>\n");
                for (SpecialCargoCntrDO cntrDO : cntrDOList) {
                    notice.append("<tr>\n");
                    notice.append("<td>").append(cntrDO.getCntrSize()).append("</td>\n");
                    notice.append("<td>").append(cntrDO.getCntrType()).append("</td>\n");
                    notice.append("<td>").append(cntrDO.getQty()).append("</td>\n");
                    notice.append("<td>").append(cntrDO.getGrossWgt()).append("</td>\n");
                    SpecialCargoFlexiDO flexiDO = cntr2FlexiMap.get(cntrDO.getId());
                    notice.append("<td>").append(flexiDO.getNameOfManufacturer()).append("</td>\n");
                    notice.append("<td>").append("Stowage Request").append("</td>\n");
                    notice.append("<td>").append(flexiDO.getCargoDescription()).append("</td>\n");
                    notice.append("</tr>\n");
                }
                notice.append("</table>");
            }
        }
        if (SpecialEnum.DG.getValue().equals(applyType)) {
            if (ApplyStatusEnum.APPROVED.getValue().equals(applyStatus)) {
                notice.append("<p>Dear Partner,</p>\n");
                notice.append("<p>Please be advised the said commodity Approval NO.").append(applyDO.getApplyNo()).append(" (your Reference No.").append(applyDO.getReferenceNo()).append(") is acceptable to be loaded on below vessel.").append("</p>\n");
                notice.append("<p>Shipper :").append(applyDO.getSpaceOwner()).append("</p>\n");
                notice.append("<p>Application type : DG</p>\n");
                notice.append("<p> Reference No.").append(applyDO.getReferenceNo()).append("</p>\n");
                for (SpecialCargoVoyageDO voyage : voyageDOList) {
                    String vesVoy = voyage.getVesselCode() + " " + voyage.getVoyageEx();
                    notice.append("<p>&nbsp;&nbsp; Vessel and Voyage number YM ").append(vesVoy).append("</p>\n");
                    notice.append("<p>&nbsp;&nbsp; POL: ").append(voyage.getPolCode()).append("</p>\n");
                    notice.append("<p>&nbsp;&nbsp; POD: ").append(voyage.getPodCode()).append("</p>\n");
                }
                notice.append("<p>Subject to approval of POD and T/s port and space available.</p>\n");
                notice.append("<p>Once you have successfully booking this cargo, the shipper should declare and ensure that the dangerous goods which is intended for carriage are accurately described with the proper shipping name(s)-PSN of classified, marked, labeled and for placard in accordance with the regulations applicable as in chapter 5.4 paragraph 5.4.1.1 of volume I of IMDG code latest edition, at the time of delivery for shipment.</p>\n");
            }
            if (ApplyStatusEnum.REJECT.getValue().equals(applyStatus)) {
                String rejectReason = voyageDOList.stream().filter(voyage -> ApplyStatusEnum.REJECT.getValue().equals(voyage.getRouteApproveStatus()))
                        .findFirst().get().getApproveRemark();
                notice.append("<p>Dear Partner,</p>\n");
                notice.append("<p>Please refer to the result and notice. If any questions or advice, please feel free to contact with SNL Special Cargo Management Team (special-cargo@cmhk.com). Thank you.</p>\n");
                notice.append("<p>We are sorry to reject your application because ").append(rejectReason).append(".</p>\n");
                notice.append("<p>Shipper :").append(applyDO.getSpaceOwner()).append("</p>\n");
                notice.append("<p>Application type : DG</p>\n");
                notice.append("<p> Reference No.").append(applyDO.getReferenceNo()).append("</p>\n");
                for (SpecialCargoVoyageDO voyage : voyageDOList) {
                    String vesVoy = voyage.getVesselCode() + " " + voyage.getVoyageEx();
                    notice.append("<p>&nbsp;&nbsp; Vessel and Voyage number YM ").append(vesVoy).append("</p>\n");
                    notice.append("<p>&nbsp;&nbsp; POL: ").append(voyage.getPolCode()).append("</p>\n");
                    notice.append("<p>&nbsp;&nbsp; POD: ").append(voyage.getPodCode()).append("</p>\n");
                }
            }
        }
        notice.append("<br>");
        notice.append("<p><b>Special Cargo Management Team</b></p>");
        notice.append("<b><p>Sinotrans Container Lines Co.,Ltd.</b></p>");
        return notice.toString();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateApprovalStatus(VoyageApprovalUpdateReqVO req) {
        SpecialCargoApplyDO applyDo = specialCargoApplyMapper.selectById(req.getApplyId());
        boolean check = !ApplyStatusEnum.CHANGEVOYCHECK.getValue().equals(applyDo.getApplyStatus()) && !ApplyStatusEnum.PENDING.getValue().equals(applyDo.getApplyStatus());
        if (check) {
            throw ServiceExceptionUtil.exception(SpecialCargoApplyErrorCodeConstants.SPECIAL_STATUS_ERROR);
        }
        if (ApprovalStatusEnum.REJECT.getValue().equals(req.getApproveStatus())) {
            OperateLogDO operateLogDO = getOperateLogDO(req);
            operateLogMapper.insert(operateLogDO);
        }
        SpecialCargoVoyageDO voyage = specialCargoVoyageMapper.selectById(req.getVoyageId());
        voyage.setApprover(String.valueOf(SecurityFrameworkUtils.getLoginUserId()));
        voyage.setRouteApproveStatus(req.getApproveStatus());
        if (req.getAddInfo() || req.getAddAttachment()) {
            voyage.setRouteApproveStatus(ApprovalStatusEnum.PENDING.getValue());
        }
        voyage.setApproveTime(LocalDateTime.now());
        voyage.setApproveRemark(req.getApproveRemark());
        specialCargoVoyageMapper.updateById(voyage);
        getSpecialCargoApplyDO(applyDo, req, voyage);
        specialCargoApplyMapper.updateById(applyDo);
        //邮件通知
        if (statusForEmail().contains(applyDo.getApplyStatus()) || (null != req.getPreConsent() && req.getPreConsent())) {
            emailTaskService.createApprovalEmailTask(applyDo.getId());
        }
        soCommonService.syncSoStatusSpecial(applyDo.getSoId(), req.getApproveStatus());
    }

    private static OperateLogDO getOperateLogDO(VoyageApprovalUpdateReqVO req) {
        OperateLogDO operateLogDO = new OperateLogDO();
        operateLogDO.setBusinessType(SpecialEnum.SPC.getValue());
        operateLogDO.setBusinessId(req.getApplyId());
        operateLogDO.setBusinessNo(req.getVoyageId().toString());
        operateLogDO.setOperateType(req.getApproveStatus());
        operateLogDO.setOperateTypeSub(req.getApproveStatus());
        operateLogDO.setOperateDesc(req.getApproveRemark());
        operateLogDO.setOperateDesc(req.getRejectReason());
        return operateLogDO;
    }

    public static List<String> statusForEmail() {
        return Arrays.asList(ApplyStatusEnum.REJECT.getValue(),
                ApplyStatusEnum.APPROVED.getValue(), ApplyStatusEnum.ADD_ATTACHMENT.getValue(), ApplyStatusEnum.ADD_INFO.getValue());
    }

    private void getSpecialCargoApplyDO(SpecialCargoApplyDO applyDO, VoyageApprovalUpdateReqVO req, SpecialCargoVoyageDO voyage) {
        if (null != req.getPreConsent() && req.getPreConsent()) {
            applyDO.setApproveStatus(ApprovalStatusEnum.PRECONSENT.getValue());
        }
        if (req.getAddInfo() && ApprovalStatusEnum.PENDING.getValue().equals(voyage.getRouteApproveStatus())) {
            applyDO.setApplyStatus(ApplyStatusEnum.ADD_INFO.getValue());
        } else if (req.getAddAttachment() && ApprovalStatusEnum.PENDING.getValue().equals(voyage.getRouteApproveStatus())) {
            applyDO.setApplyStatus(ApplyStatusEnum.ADD_ATTACHMENT.getValue());
        } else {
            List<SpecialCargoVoyageDO> voyageDos = specialCargoVoyageMapper.selectList(new LambdaQueryWrapper<SpecialCargoVoyageDO>().eq(SpecialCargoVoyageDO::getApplyId, req.getApplyId()));
            Set<String> statusSet = voyageDos.stream().map(SpecialCargoVoyageDO::getRouteApproveStatus).collect(Collectors.toSet());
            if (statusSet.contains(ApprovalStatusEnum.APPROVED.getValue())) {
                applyDO.setApplyStatus(ApplyStatusEnum.APPROVED.getValue());
            }
            if (statusSet.contains(ApprovalStatusEnum.PENDING.getValue())) {
                //如果是修改船名航次二次审批状态，在审批或者拒绝后依然是这个状态
                if (ApplyStatusEnum.CHANGEVOYCHECK.getValue().equals(applyDO.getApplyStatus())) {
                    applyDO.setApplyStatus(ApplyStatusEnum.CHANGEVOYCHECK.getValue());
                } else {
                    applyDO.setApplyStatus(ApplyStatusEnum.PENDING.getValue());
                }
            }
            if (statusSet.contains(ApprovalStatusEnum.REJECT.getValue())) {
                applyDO.setApplyStatus(ApplyStatusEnum.REJECT.getValue());
            }
        }
    }

    @Override
    public CntrDataRespVO getCntrData(SpecialCargoApplyPageReqVO reqVO) {
        if (reqVO.getEndDate() != null) {
            // 默认结束时间包括当天
            reqVO.setEndDate(reqVO.getEndDate().plusDays(1));
        }
        List<CntrDataRespVO> dataInfo = specialCargoApplyMapper.getCntrDataInfo(reqVO);
        int size20Num = dataInfo.stream()
                .filter(vo -> vo.getCntrSize().startsWith(CNTR_SIZE_20))
                .mapToInt(CntrDataRespVO::getNum).sum();
        int size40Num = dataInfo.stream()
                .filter(vo -> vo.getCntrSize().startsWith(CNTR_SIZE_40))
                .mapToInt(CntrDataRespVO::getNum).sum();
        CntrDataRespVO respVO = new CntrDataRespVO();
        respVO.setSizeTwentyNum(size20Num);
        respVO.setSizeFortyNum(size40Num);
        respVO.setTeuNum(size20Num + 2 * size40Num);
        return respVO;
    }
}
